/*
 * 2011 Runedev development team
 * http://lazygamerz.org
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser Gereral Public Licence as published by the Free
 * Software Foundation; either version 3 of the Licence, or (at your opinion) any
 * later version.
 *
 * This library is distributed in the hope that it will be usefull, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of merchantability or fitness for a
 * particular purpose. See the GNU Lesser General Public Licence for more details.
 *
 * You should have received a copy of the GNU Lesser General Public Licence along
 * with this library; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, Ma 02111-1307 USA.
 *
 * http://www.gnu.org/licenses/lgpl.html (English)
 * http://gugs.sindominio.net/gnu-gpl/lgpl-es.html (Espa�ol)
 *
 */
package org.lazygamerz.scripting.api;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.rsbot.bot.Bot;
import org.rsbot.script.wrappers.RSInterface;
import org.rsbot.script.wrappers.RSInterfaceChild;
import org.rsbot.util.ColorUtil;

/**
 * Provides access to game interfaces.
 * @author Runedev development team - version 1.0
 */
public class Interface {
    private final java.util.Random random = new java.util.Random();

    public Interface() {
    }

    /* A cache of all the interfaces. Only as big as the maximum size of the client's cache. */
    private RSInterface[] mainCache = new RSInterface[0];
    /* If it doesn't fit in the above cache. */
    private final Map<Integer, RSInterface> sparseMap = new HashMap<Integer, RSInterface>();
    
    /**
     * @return <tt>true</tt> if continue component is valid; otherwise
     *         <tt>false</tt>.
     */
    public boolean canContinue() {
        return getContinueChild() != null;
    }
    
    /**
     * Attempts to click all the components.
     *
     * @param leftclick     true to left-click; false to right click.
     * @param ids           All the components to be clicked.
     * @return              <tt>true</tt> if all components were clicked; otherwise
     *                      <tt>false</tt>.
     */
    public boolean click(boolean leftclick, RSInterfaceChild... ids) {
        boolean did = true;
        for (RSInterfaceChild com : ids) {
            if (!com.isValid()) {
                return false;
            }
            if (com.getPoint() == null || com.getPoint().x == -1
                    || com.getPoint().y == -1) {
                return false;
            }
            if (!clickChild(com, leftclick)) {
                did = false;
            }
        }
        return did;
    }

    /**
     * Clicks the dialogue option that contains the desired string.
     *
     * @param inter             The interface of the dialogue menu.
     * @param option            The text we want to click.
     * @return <tt>true</tt>    if the option was clicked; otherwise <tt>false</tt>
     *         .
     */
    public boolean click(final RSInterface inter, String option) {
        /**
         * This is superfluous but it just makes life a little easier
         * so you don't have to look up the component.
         * Just grab the interface and the text you want to click.
         */
        if (inter.isValid()) {
            option = option.toLowerCase();
            for (final RSInterfaceChild c : inter.getChildren()) {
                if (c.getText().toLowerCase().contains(option)) {
                    return c.click();
                }
            }
        }
        return false;
    }

    /**
     * Clicks the component and then the action in the menu.
     *
     * @param ChildInterface    The child containing the component.
     * @param ComponentID       The component to be clicked.
     * @param act               The menu action to be done.
     * @return                  <tt>true</tt> if the action was clicked;
     *                          otherwise <tt>false</tt>
     *         .
     */
    public boolean clickChild(RSInterfaceChild child, int id, String act) {
        return clickChild(child, id, false) && Bot.menu.action(act);
    }

    /**
     * Clicks an RSComponent.
     *
     * @param child         The child containing the component.
     * @paramid             The component to be clicked.
     * @param leftclick     true to left-click, false to right-click.
     * @return              true if it successfully clicked the component.
     */
    public boolean clickChild(RSInterfaceChild child, int id, boolean leftclick) {
        if (!child.isValid()) {
            return false;
        }
        RSInterfaceChild[] coms = child.getChildren();
        if (coms.length == 0 || coms.length < id - 1) {
            return false;
        }
        RSInterfaceChild com = coms[id];
        if (com == null || com.getPoint() == null || com.getPoint().x == -1
                || com.getPoint().y == -1) {
            return false;
        }
        Bot.mouse.click(new Point(com.getPoint().x + random(-5, 6),
                com.getPoint().y + random(-5, 6)), leftclick);
        return true;
    }

    /**
     * Left-clicks the child interface with the given parent ID and child ID if
     * it is showing (valid).
     *
     * @param iface             The parent interface ID.
     * @param child             The child interface ID.
     * @return <tt>true</tt>    if the action was clicked; otherwise false.
     * @see                     #atInterface(RSInterfaceChild, String)
     * @see                     #atInterface(int, int, String)
     */
    public boolean clickChild(int face, int child) {
        return clickChild(getChild(face, child));
    }

    /**
     * Performs the provided action on the child interface with the given parent
     * ID and child ID if it is showing (valid).
     *
     * @param iface     The parent interface ID.
     * @param child     The child interface ID.
     * @param act       The menu action to click.
     * @return          <tt>true</tt> if the action was clicked; otherwise false.
     * @see             #atInterface(RSInterfaceChild, String)
     * @see             #atInterface(int, int)
     */
    public boolean clickChild(int face, int child, String act) {
        return clickChild(getChild(face, child), act);
    }

    /**
     * Left-clicks the provided RSInterfaceChild if it is showing (valid).
     *
     * @param i                 The child interface to click.
     * @return <tt>true</tt>    if the action was clicked; otherwise false.
     * @see                     #atInterface(RSInterfaceChild, String)
     */
    public boolean clickChild(RSInterfaceChild child) {
        return clickChild(child, true);
    }

    public boolean clickChild(RSInterfaceChild com, boolean leftClick) {
        if (com == null || com.getPoint() == null || com.getPoint().x == -1
                || com.getPoint().y == -1) {
            return false;
        }

        Bot.mouse.click(new Point(com.getPoint().x + random(-5, 6),
                com.getPoint().y + random(-5, 6)), leftClick);
        return true;
    }

    /**
     * Performs the given action on the provided RSInterfaceChild if it is
     * showing (valid).
     *
     * @param child             The child interface to click.
     * @param act               The menu action to click.
     * @return <tt>true</tt>    if the action was clicked; otherwise false.
     * @see                     #atInterface(RSInterfaceChild)
     * @see                     #atInterface(int, int, String)
     */
    public boolean clickChild(RSInterfaceChild c, String act) {
        if (!c.isValid()) {
            return false;
        }
        final Rectangle rect = c.getArea();
        if (rect.x == -1) {
            return false;
        }
        /* 1 pixel is not enough for all components */
        final int minX = rect.x + 2, minY = rect.y + 2, width = rect.width - 4, height = rect.height - 4;
        final Rectangle actual = new Rectangle(minX, minY, width, height);
        /* Check if the menu already contains the action otherwise reposition before clicking */
        if (actual.contains(Bot.mouse.getLocation()) && Bot.menu.action(act)) {
            return true;
        }
        Bot.mouse.move(random(minX, minX + width),
                random(minY, minY + height));
        return Bot.menu.action(act);
    }

    /**
     * @return <tt>true</tt> if continue component was clicked; otherwise
     *         <tt>false</tt>.
     */
    public boolean clickContinue() {
        final RSInterfaceChild cont = getContinueChild();
        return cont != null && cont.isValid() && cont.click(true);
    }

    /**
     * @return <code>RSInterface</code> array containing all valid interfaces.
     */
    public synchronized RSInterface[] getAll() {
        enlargeCache();
        final org.rsbot.client.RSInterface[][] inters = Bot.game.client().getRSInterfaceCache();
        if (inters == null) {
            return new RSInterface[0];
        }
        final List<RSInterface> out = new ArrayList<RSInterface>();
        for (int i = 0; i < inters.length; i++) {
            if (inters[i] != null) {
                final RSInterface in = get(i);
                if (in.isValid()) {
                    out.add(in);
                }
            }
        }
        return out.toArray(new RSInterface[out.size()]);
    }

    /**
     * @param index The index of the interface.
     * @return The <tt>RSInterface</tt> for the given index.
     */
    public synchronized RSInterface get(final int index) {
        RSInterface inter;
        final int cacheLen = mainCache.length;
        if (index < cacheLen) {
            inter = mainCache[index];
            if (inter == null) {
                inter = new RSInterface(index);
                mainCache[index] = inter;
            }
        } else {
            inter = sparseMap.get(index);
            if (inter == null) {
                enlargeCache();
                if (index < cacheLen) {
                    inter = mainCache[index];
                    if (inter == null) {
                        inter = new RSInterface(index);
                        mainCache[index] = inter;
                    }
                } else {
                    inter = new RSInterface(index);
                    sparseMap.put(index, inter);
                }
            }
        }
        return inter;
    }

    public Rectangle getBoxArea(RSInterfaceChild com) {
        Rectangle boxArea = new Rectangle(com.getAbsoluteX(),
                com.getAbsoluteY(), com.getWidth(), com.getHeight());
        return boxArea;
    }

    /**
     * @param index      The parent interface index
     * @param childIndex The component index
     * @return <tt>RSComponent</tt> for the given index and child index.
     */
    public RSInterfaceChild getChild(final int index, final int childIndex) {
        return get(index).getChild(childIndex);
    }

    /**
     * @param id The packed interface index ((x << 16) | (y & 0xFFFF)).
     * @return <tt>RSComponent</tt> for the given interface id.
     */
    public RSInterfaceChild getChild(final int id) {
        final int x = id >> 16;
        final int y = id & 0xFFFF;
        return get(x).getChild(y);
    }
    
    /**
     * get the interface that contains the specific text
     * @param text the text that is being looked for in the interfaces
     * @return true if text is found
     */
    public RSInterface[] getContainingText(String text) {
        List<RSInterface> results = new LinkedList<RSInterface>();
        for (RSInterface face : getAll()) {
            if (face.getText().toLowerCase().contains(text.toLowerCase())) {
                results.add(face);
            }
        }
        return results.toArray(new RSInterface[results.size()]);
    }
    
    public RSInterface getContinue() {
        if (Bot.game.client().getRSInterfaceCache() == null) {
            return null;
        }
        RSInterface[] valid = getAll();
        for (RSInterface face : valid) {
            if (face.containsText("Click here to continue")) {
                return face;
            }
        }
        return null;
    }

    /**
     * @return <tt>RSComponent</tt> containing "Click here to continue";
     *         otherwise null.
     */
    public RSInterfaceChild getContinueChild() {
        if (Bot.game.client().getRSInterfaceCache() == null) {
            return null;
        }
        final RSInterface[] valid = getAll();
        for (final RSInterface iface : valid) {
            if (iface.getIndex() != 137) {
                final int len = iface.getChildCount();
                for (int i = 0; i < len; i++) {
                    final RSInterfaceChild child = iface.getChild(i);
                    if (child.containsText("Click here to continue") && child.isValid() && child.getAbsoluteX() > 10 && child.getAbsoluteY() > 300) {
                        return child;
                    }
                }
            }
        }
        return null;
    }

    /**
     * @return The maximum known interface cache size.
     */
    public synchronized int getMaxCacheSize() {
        enlargeCache();
        return mainCache.length;
    }

    /**
     * @param text                      The text to search each interface for.
     * @return <tt>RSInterface</tt>     array of the interfaces containing specified
     *         text.
     */
    public RSInterface[] getAllContaining(final String text) {
        final java.util.List<RSInterface> results = new LinkedList<RSInterface>();
        for (final RSInterface iface : getAll()) {
            if (iface.getText().toLowerCase().contains(text.toLowerCase())) {
                results.add(iface);
            }
        }
        return results.toArray(new RSInterface[results.size()]);
    }

    public boolean isTextInputOpen() {
        if (!getChild(137, 55).containsText(
                Bot.player.getMine().getName())) {
            return false;
        }
        RSInterfaceChild put = getChild(137, 54);
        if (put.isValid()) {
            Rectangle area = put.getArea();
            area.width += 100;
            Point[] points = 
            	ColorUtil.findColorInArea(area, new Color(219, 219, 219), 
            			                new Color(10, 10, 10));
            if (points != null && points.length > 5) {
                return false;
            } else {
                return true;
            }
        }
        return false;
    }

    /**
     * Scrolls to the component
     *
     * @param component         component to scroll to
     * @param scrollBarID       scrollbar to scroll with
     * @return true             when scrolled successfully
     */
    public boolean scrollTo(final RSInterfaceChild component, final int scrollBarID) {
        final RSInterfaceChild scrollBar = getChild(scrollBarID);

        return scrollTo(component, scrollBar);
    }

    /**
     * Scrolls to the component
     *
     * @param component         component to scroll to
     * @param scrollBar         scrollbar to scroll with
     * @return true             when scrolled successfully
     */
    public boolean scrollTo(final RSInterfaceChild component, final RSInterfaceChild scrollBar) {
        /* Check arguments */
        if (component == null || scrollBar == null || !component.isValid()) {
            return false;
        }

        if (scrollBar.getChildren().length != 6) {
            /* no scrollbar, so probably not scrollable */
            return true;
        }

        /* Find scrollable area */
        RSInterfaceChild scrollableArea = component;
        while (scrollableArea.getScrollableContentHeight() == 0
                && scrollableArea.getParentID() != -1) {
            scrollableArea = getChild(scrollableArea.getParentID());
        }

        /* Check scrollable area */
        if (scrollableArea.getScrollableContentHeight() == 0) {
            return false;
        }

        /* Get scrollable area height */
        final int areaY = scrollableArea.getAbsoluteY();
        final int areaHeight = scrollableArea.getRealHeight();

        /* Check if the component is already visible */
        if (component.getAbsoluteY() >= areaY
                && component.getAbsoluteY() <= areaY + areaHeight
                - component.getRealHeight()) {
            return true;
        }

        /* Calculate scroll bar position to click */
        final RSInterfaceChild scrollBarArea = scrollBar.getChild(0);
        final int contentHeight = scrollableArea.getScrollableContentHeight();

        int pos = (int) ((float) scrollBarArea.getRealHeight() / contentHeight * (component.getRelativeY()
                + random(-areaHeight / 2, areaHeight / 2 - component.getRealHeight())));
        /* inner */
        if (pos < 0) {
            pos = 0;
        } else if (pos >= scrollBarArea.getRealHeight()) {
            pos = scrollBarArea.getRealHeight() - 1; // outer
        }

        /* Click on the scrollbar */
        Bot.mouse.click(scrollBarArea.getAbsoluteX()
                + random(0, scrollBarArea.getRealWidth()), scrollBarArea.getAbsoluteY() + pos, true);

        /* Wait a bit */
        Bot.game.sleep(random(200, 400));

        /* Scroll to it if we missed it */
        while (component.getAbsoluteY() < areaY || component.getAbsoluteY() > areaY
                + areaHeight - component.getRealHeight()) {
            final boolean scrollUp = component.getAbsoluteY() < areaY;
            scrollBar.getChild(scrollUp ? 4 : 5).action("");

            Bot.game.sleep(random(100, 200));
        }

        /* Return whether or not the component is visible now. */
        return component.getAbsoluteY() >= areaY && component.getAbsoluteY() <= areaY
                + areaHeight - component.getRealHeight();
    }

    /**
     * Enlarges the cache if there are more interfaces than the cache size.
     */
    public synchronized void enlargeCache() {
        final org.rsbot.client.RSInterface[][] inters = Bot.game.client().getRSInterfaceCache();
        if (inters != null && mainCache.length < inters.length) {
            /*enlarge cache */
            mainCache = Arrays.copyOf(mainCache, inters.length);
            for (int i = mainCache.length; i < mainCache.length; i++) {
                final RSInterface tmp = sparseMap.get(i);
                if (tmp != null) {
                    sparseMap.remove(i);
                    mainCache[i] = tmp;
                }
            }
        }
    }

    /**
     * Waits for an interface to be closed/opened.
     *
     * @param face The interface to wait for.
     * @param timer Milliseconds to wait for the interface to open/close.
     * @param valid True if open, false if close.
     * @return <tt>true</tt> if the interface was successfully closed/opened.
     */
    public boolean waitFor(final RSInterface face, final int timer, final boolean valid) {
        for (int w = 0; w < timer && face.isValid() == valid ? true : false; w++) {
            Bot.game.sleep(1);
        }
        return face.isValid() == valid ? true : false;
    }
    
    /**
     * waits for the the interface to be close
     * 
     * @param face      the interface to wait for
     * @param ms        the amount of time to wait
     * @return false     action being waited for 
     */
    public boolean waitForClose(RSInterface face, int ms) {
        return waitFor(face, ms, false);
    }
    
    /**
     * waits for the the interface to be open
     * 
     * @param face      the interface to wait for
     * @param ms        the amount of time to wait
     * @return true     action being waited for 
     */
    public boolean waitForOpen(RSInterface face, int ms) {
        return waitFor(face, ms, true);
    }
    
    /**
     * Waits for an interface to be closed/opened.
     *
     * @param child             The interface child to wait for.
     * @param timer             Milliseconds to wait for the interface to open/close.
     * @param valid             True if open, false if close.
     * @return <tt>true</tt>    if the interface was successfully closed/opened.
     */
    public boolean waitForChild(final RSInterfaceChild child, final int timer, final boolean valid) {
        for (int w = 0; w < timer && child.isValid() == valid ? true : false; w++) {
            Bot.game.sleep(1);
        }
        return child.isValid() == valid ? true : false;
    }
    
    /**
     * waits for the interface child to be not valid or closed on the screen
     * 
     * @param child             interface child to wait for
     * @param ms                the amount of tiem to wait
     * @return <tt>true</tt>    if opened
     */
    public boolean waitForChildClose(RSInterfaceChild child, int ms) {
        return waitForChild(child, ms, false);
    }
    
    /**
     * waits for the interface child to be valid on the screen
     * 
     * @param child             interface child to wait for
     * @param ms                the amount of tiem to wait
     * @return <tt>true</tt>    if opened
     */
    public boolean waitForChildOpen(RSInterfaceChild child, int ms) {
        return waitForChild(child, ms, true);
    }

    /**
     * Returns a random double in the specified range with a Gaussian
     * distribution having a mean of the absolute value of the difference
     * between min and max and a standard deviation of 1.  The returned
     * value will honor the min and max boundaries.
     *
     * @param min   Minimum value (inclusive).
     * @param max   Maximum value (inclusive).
     * @return      The random <code>double</code> generated.
     */
    public double random(double min, double max) {
    	double mean = min + Math.abs(max-min)/2;

    	double next;
    	
    	// The loop ensures we honor the min/max values.
    	do  {
    		next = mean + random.nextGaussian();
    	} while (next<min || next>max);    	 
    	
        return next;
    }

    /**
     * Returns a random int in the specified range with a Gaussian
     * distribution having a mean of the absolute value of the difference
     * between min and max and a standard deviation of 1.  The returned
     * value will honor the min and max boundaries.
     *
     * @param min   Minimum value (inclusive).
     * @param max   Maximum value (inclusive).
     * @return      The random <code>int</code> generated.
     */
    public int random(int min, int max) {
    	int mean = min + Math.abs(max-min)/2;
    	int next;
    	
    	// The loop ensures we honor the min/max values.
    	do  {
    		next = (int) (mean + random.nextGaussian());
    	} while (next<min || next>max);    	 
    	
        return next;
    }

    /**
     * Returns a random int in the specified range with a Gaussian
     * distribution having a mean of the absolute value of the difference
     * between min and max and a standard deviation of sd.  The returned
     * value will honor the min and max boundaries.
     *
     * @param min   Minimum value (inclusive).
     * @param max   Maximum value (inclusive).
     * @return      The random <code>int</code> generated.
     */
    public int random(int min, int max, int sd) {
        int mean = min + Math.abs(max - min) / 2;
        int next;
        do {
        	next = (int) (mean + random.nextGaussian() * sd);
        } while (next < min || next > max);
        return next;
    }
}
